/*
 * Copyright 2016 WebAssembly Community Group participants
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//
// A WebAssembly optimizer, loads code, optionally runs passes on it,
// then writes it.
//

#include <memory>

#include "pass.h"
#include "support/command-line.h"
#include "support/file.h"
#include "wasm-printing.h"
#include "wasm-s-parser.h"
#include "wasm-validator.h"
#include "wasm-io.h"
#include "wasm-interpreter.h"
#include "wasm-binary.h"
#include "shell-interface.h"
#include "optimization-options.h"
#include "execution-results.h"
#include "fuzzing.h"
#include "js-wrapper.h"
#include "spec-wrapper.h"

using namespace wasm;

// runs a command and returns its output TODO: portability, return code checking
std::string runCommand(std::string command) {
#ifdef __linux__
  std::string output;
  const int MAX_BUFFER = 1024;
  char buffer[MAX_BUFFER];
  FILE *stream = popen(command.c_str(), "r");
  while (fgets(buffer, MAX_BUFFER, stream) != NULL) {
    output.append(buffer);
  }
  pclose(stream);
  return output;
#else
  Fatal() << "TODO: portability for wasm-opt runCommand";
#endif
}

//
// main
//

int main(int argc, const char* argv[]) {
  Name entry;
  bool emitBinary = true;
  bool debugInfo = false;
  bool fuzzExec = false;
  bool fuzzBinary = false;
  std::string extraFuzzCommand;
  bool translateToFuzz = false;
  bool fuzzPasses = false;
  std::string emitJSWrapper;
  std::string emitSpecWrapper;

  OptimizationOptions options("wasm-opt", "Read, write, and optimize files");
  options
      .add("--output", "-o", "Output file (stdout if not specified)",
           Options::Arguments::One,
           [](Options* o, const std::string& argument) {
             o->extra["output"] = argument;
             Colors::disable();
           })
      .add("--emit-text", "-S", "Emit text instead of binary for the output file",
           Options::Arguments::Zero,
           [&](Options *o, const std::string& argument) { emitBinary = false; })
      .add("--debuginfo", "-g", "Emit names section and debug info",
           Options::Arguments::Zero,
           [&](Options *o, const std::string& arguments) { debugInfo = true; })
      .add("--fuzz-exec", "-fe", "Execute functions before and after optimization, helping fuzzing find bugs",
           Options::Arguments::Zero,
           [&](Options *o, const std::string& arguments) { fuzzExec = true; })
      .add("--fuzz-binary", "-fb", "Convert to binary and back after optimizations and before fuzz-exec, helping fuzzing find binary format bugs",
           Options::Arguments::Zero,
           [&](Options *o, const std::string& arguments) { fuzzBinary = true; })
      .add("--extra-fuzz-command", "-efc", "An extra command to run on the output before and after optimizing. The output is compared between the two, and an error occurs if they are not equal",
           Options::Arguments::One,
           [&](Options *o, const std::string& arguments) { extraFuzzCommand = arguments; })
      .add("--translate-to-fuzz", "-ttf", "Translate the input into a valid wasm module *somehow*, useful for fuzzing",
           Options::Arguments::Zero,
           [&](Options *o, const std::string& arguments) { translateToFuzz = true; })
      .add("--fuzz-passes", "-fp", "Pick a random set of passes to run, useful for fuzzing. this depends on translate-to-fuzz (it picks the passes from the input)",
           Options::Arguments::Zero,
           [&](Options *o, const std::string& arguments) { fuzzPasses = true; })
      .add("--emit-js-wrapper", "-ejw", "Emit a JavaScript wrapper file that can run the wasm with some test values, useful for fuzzing",
           Options::Arguments::One,
           [&](Options *o, const std::string& arguments) { emitJSWrapper = arguments; })
      .add("--emit-spec-wrapper", "-esw", "Emit a wasm spec interpreter wrapper file that can run the wasm with some test values, useful for fuzzing",
           Options::Arguments::One,
           [&](Options *o, const std::string& arguments) { emitSpecWrapper = arguments; })
      .add_positional("INFILE", Options::Arguments::One,
                      [](Options* o, const std::string& argument) {
                        o->extra["infile"] = argument;
                      });
  options.parse(argc, argv);

  Module wasm;
  // It should be safe to just always enable atomics in wasm-opt, because we
  // don't expect any passes to accidentally generate atomic ops
  FeatureSet features = Feature::Atomics;

  if (options.debug) std::cerr << "reading...\n";

  if (!translateToFuzz) {
    ModuleReader reader;
    reader.setDebug(options.debug);
    try {
      reader.read(options.extra["infile"], wasm);
    } catch (ParseException& p) {
      p.dump(std::cerr);
      Fatal() << "error in parsing input";
    } catch (std::bad_alloc& b) {
      Fatal() << "error in building module, std::bad_alloc (possibly invalid request for silly amounts of memory)";
    }

    if (!WasmValidator().validate(wasm, features)) {
      WasmPrinter::printModule(&wasm);
      Fatal() << "error in validating input";
    }
  } else {
    // translate-to-fuzz
    TranslateToFuzzReader reader(wasm, options.extra["infile"]);
    if (fuzzPasses) {
      reader.pickPasses(options);
    }
    reader.build();
    if (!WasmValidator().validate(wasm, features)) {
      WasmPrinter::printModule(&wasm);
      std::cerr << "translate-to-fuzz must always generate a valid module";
      abort();
    }
  }

  ExecutionResults results;
  if (fuzzExec) {
    results.get(wasm);
  }

  if (emitJSWrapper.size() > 0) {
    std::ofstream outfile;
    outfile.open(emitJSWrapper, std::ofstream::out);
    outfile << generateJSWrapper(wasm);
    outfile.close();
  }

  if (emitSpecWrapper.size() > 0) {
    std::ofstream outfile;
    outfile.open(emitSpecWrapper, std::ofstream::out);
    outfile << generateSpecWrapper(wasm);
    outfile.close();
  }

  std::string firstOutput;

  if (extraFuzzCommand.size() > 0 && options.extra.count("output") > 0) {
    if (options.debug) std::cerr << "writing binary before opts, for extra fuzz command..." << std::endl;
    ModuleWriter writer;
    writer.setDebug(options.debug);
    writer.setBinary(emitBinary);
    writer.setDebugInfo(debugInfo);
    writer.write(wasm, options.extra["output"]);
    firstOutput = runCommand(extraFuzzCommand);
    std::cout << "[extra-fuzz-command first output:]\n" << firstOutput << '\n';
  }

  Module* curr = &wasm;
  Module other;

  if (fuzzExec && fuzzBinary) {
    BufferWithRandomAccess buffer(false);
    // write the binary
    WasmBinaryWriter writer(&wasm, buffer, false);
    writer.write();
    // read the binary
    auto input = buffer.getAsChars();
    WasmBinaryBuilder parser(other, input, false);
    parser.read();
    bool valid = WasmValidator().validate(other, features);
    if (!valid) {
      WasmPrinter::printModule(&other);
    }
    assert(valid);
    curr = &other;
  }

  if (options.runningPasses()) {
    if (options.debug) std::cerr << "running passes...\n";
    options.runPasses(*curr);
    bool valid = WasmValidator().validate(*curr, features);
    if (!valid) {
      WasmPrinter::printModule(&*curr);
    }
    assert(valid);
  }

  if (fuzzExec) {
    results.check(*curr);
  }

  if (options.extra.count("output") > 0) {
    if (options.debug) std::cerr << "writing..." << std::endl;
    ModuleWriter writer;
    writer.setDebug(options.debug);
    writer.setBinary(emitBinary);
    writer.setDebugInfo(debugInfo);
    writer.write(*curr, options.extra["output"]);

    if (extraFuzzCommand.size() > 0) {
      auto secondOutput = runCommand(extraFuzzCommand);
      std::cout << "[extra-fuzz-command second output:]\n" << firstOutput << '\n';
      if (firstOutput != secondOutput) {
        std::cerr << "extra fuzz command output differs\n";
        abort();
      }
    }
  }
}
